Skip to content

node:fs: use node's ERR_INVALID_ARG_TYPE message for invalid utimes/lutimes/futimes times - #38669

Open
robobun wants to merge 1 commit into
mainfrom
farm/02712898/utimes-invalid-time-message
Open

robobun wants to merge 1 commit into
mainfrom
farm/02712898/utimes-invalid-time-message

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • fs.utimesSync(file, "bad", 1) throws TypeError [ERR_INVALID_ARG_TYPE]: atime must be a number or a Date. Node v26.3.0 throws The "time" argument must be an instance of Date or an Time in seconds. Received type string ('bad'). Class and code already match; only the message differs.
  • It differs in every form: utimes, lutimes and futimes, sync, callback, fs/promises and FileHandle.utimes, for atime and for mtime. A missing argument says atime is required where node says the same message with Received undefined.
  • The argument name is also wrong: node says "time" in utimes/lutimes and "atime"/"mtime" only in futimes (and therefore FileHandle.utimes); bun said atime/mtime everywhere.
  • Cause: args::Lutimes::from_js (also used for utimes, args::Utimes is an alias of it) and args::Futimes::from_js in src/runtime/node/node_fs.rs format their own text with throw_invalid_arguments instead of building the ERR_INVALID_ARG_TYPE message with the received value.

Fix

  • Adds time_like::from_js_required(ctx, arguments, name) in src/runtime/node/time_like.rs: eats the next argument (undefined when it is missing, which is what node's validator sees too) and, when the existing time_like::from_js rejects it, throws through JSGlobalObject::throw_invalid_argument_type_value2 with node's expected-type phrase. The four sites in node_fs.rs call it; utimes/lutimes pass "time", futimes passes "atime" and "mtime".
  • Why this is right: it matches node. Node's toUnixTimestamp throws ERR_INVALID_ARG_TYPE(name, ['Date', 'Time in seconds'], time), and node's list renderer prints that array as an instance of Date or an Time in seconds; the an Time is node's own output and is reproduced on purpose (same approach as the an Promise message in src/runtime/webcore/wasm_streaming.rs). The Received ... half comes from the same determineSpecificType port that every other ERR_INVALID_ARG_TYPE in bun uses, so it already matches node for strings, objects, null, undefined, NaN, bigints and so on.
  • Accepted inputs do not change: time_like::from_js is untouched, only its rejection is reported differently.
  • Verified:
    • test/js/node/fs/fs.test.ts, new describe("utimes/lutimes/futimes reject an invalid time with node's message"): ten invalid values across utimes/lutimes/futimes for both atime and mtime, the callback forms, fs/promises and FileHandle.utimes, missing arguments, and atime being reported before mtime. All 14 tests fail on bun 1.4.0 and pass with this change. Every expected string is what node v26.3.0 prints for the same call (see the details block).
    • Whole fs.test.ts with the debug build: 524 pass. The one failure is the readdirSync ... x 100 test hitting its 10s timeout under the debug build here; it does not touch this code.
    • Vendored test-fs-utimes.js, test-fs-timestamp-parsing-error.js, test-fs-utimes-y2K38.js, test-fs-promises.js and the four test-fs-cp-*-preserve-timestamps* tests still exit 0 (they cover the accepted inputs).
  • Related: node: pass ERR_INVALID_ARG_TYPE expected types as lists so fs streams, zlib and diagnostics_channel messages match node #38657 fixes the same wording in the JS helper fs._toUnixTimestamp (src/js/node/fs.ts). The real fs.utimes* functions validate in node_fs.rs, which is what this PR changes; the two do not touch the same lines.

Background

  • ERR_INVALID_ARG_TYPE message shape: node renders The "<name>" argument must be <expected>. Received <value>. <expected> is built from an array of entries: class names become an instance of X, any other phrase is printed as written with an in front when it starts with a capital, and the groups are joined with or. <value> comes from determineSpecificType, e.g. type string ('bad'), an instance of Object, undefined. Bun ports both in src/jsc/bindings/ErrorCode.cpp; from Rust, throw_invalid_argument_type_value2(name, phrase, value) takes the already rendered <expected> phrase and renders <value> through that port.
  • toUnixTimestamp(time, name = 'time') in node's lib/internal/fs/utils.js turns a finite number, a numeric string or a Date into seconds and throws the error above for anything else. fs.utimes and fs.lutimes call it with the default name; fs.futimes passes 'atime' and 'mtime'; FileHandle.utimes is implemented with futimes. Bun's equivalent is time_like::from_js, called from the args::*::from_js argument parsers that the sync, callback and promise forms of a node:fs function share, which is why one change covers every form.
bun 1.4.0 vs node v26.3.0 (same output for the callback and promise forms)
fs.utimesSync(f, "bad", 1)
  bun:  atime must be a number or a Date
  node: The "time" argument must be an instance of Date or an Time in seconds. Received type string ('bad')
fs.utimesSync(f, 1, {})
  bun:  mtime must be a number or a Date
  node: The "time" argument must be an instance of Date or an Time in seconds. Received an instance of Object
fs.lutimesSync(f, null, 1)
  bun:  atime must be a number or a Date
  node: The "time" argument must be an instance of Date or an Time in seconds. Received null
fs.futimesSync(fd, "bad", 1)
  bun:  atime must be a number or a Date
  node: The "atime" argument must be an instance of Date or an Time in seconds. Received type string ('bad')
fs.futimesSync(fd, 1, NaN)
  bun:  mtime must be a number or a Date
  node: The "mtime" argument must be an instance of Date or an Time in seconds. Received type number (NaN)
fs.utimesSync(f, 1)
  bun:  mtime is required
  node: The "time" argument must be an instance of Date or an Time in seconds. Received undefined
handle.utimes("bad", 1)   (FileHandle)
  bun:  atime must be a number or a Date
  node: The "atime" argument must be an instance of Date or an Time in seconds. Received type string ('bad')

A script exercising the six functions (sync, callback, promises, FileHandle) with 18 different invalid values prints identical output under this build and under node, with one pre-existing exception that this PR leaves alone: for an invalid Date (new Date(NaN)) node passes NaN through to the binding and leaves the timestamp untouched, while bun rejects it (now with the message above, Received an instance of Date). That is a question of whether to throw, not of the message, so it is not covered by the new tests.

…utimes/futimes times

utimes, lutimes and futimes (sync, callback, promises and FileHandle.utimes)
threw ERR_INVALID_ARG_TYPE with the message "atime must be a number or a
Date" (or "atime is required" when the argument was missing). Node throws
the message produced by toUnixTimestamp: 'The "time" argument must be an
instance of Date or an Time in seconds. Received ...', where utimes and
lutimes use the name "time" and futimes uses "atime"/"mtime".

Add time_like::from_js_required, which eats the next argument (undefined
when missing) and throws that message, and use it at the four call sites.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96388eb6-4db8-441d-9015-8b246b514615

📥 Commits

Reviewing files that changed from the base of the PR and between 9cff2a1 and d3236fc.

📒 Files selected for processing (3)
  • src/runtime/node/node_fs.rs
  • src/runtime/node/time_like.rs
  • test/js/node/fs/fs.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on bun 1.4.0 and on main, fix pushed in this PR, self-review in progress.

Reproduced by running fs.utimesSync(f, "bad", 1), the lutimes/futimes, callback, fs/promises and FileHandle.utimes variants under bun and node v26.3.0 and diffing the messages (table in the PR body). The new tests in test/js/node/fs/fs.test.ts fail on bun 1.4.0 (14 fail) and pass with this branch.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant